#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/time.h>
#include <sys/socket.h>
#include "network.h"
#include "action.h"
#include "bailout.h"
#include "main.h"

#define PULSE_INTERVAL (FPS/2)
#if TOWERHACK_DEBUG
#define DISC_TIMELIMIT (FPS*60)
#else
#define DISC_TIMELIMIT (FPS*10)
#endif

struct addrinfo* serveraddr;
int sockhdl; /* socket handle */
int seq = 1; /* sequence number for outgoing packets */
int seq_in = 0; /* sequence number for incoming packets */
int connected = 0;
int my_uid; /* this clients unique ID which identifies it to the server */
int disc_countdown; // Ticks until we consider ourselves disconnected from the server
int pulse_countdown; // Ticks until we send a new pulse signal

/* All the clients which are currently connected and their players */
Client peer [MAX_CLIENTS];
Player player [MAX_CLIENTS]; // player[x] does not necessarily belong to peer[x]
int owner [MAX_CLIENTS]; // but player[x] belongs to client[owner[x]]

/* Funny callbacks! */
void (*notify_clientjoin) (int index) = 0;
void (*notify_playerjoin) (int index) = 0;
void (*notify_clientleave) (int index) = 0;
void (*notify_playerleave) (int index) = 0;

/* initializes the socket and server address info after the user has entered a server host name */
void network_init (const char* servername)
{
	srand (time(0) ^ *( (unsigned*) servername));

	#if TOWERHACK_DEBUG
	fprintf (stderr, "[Debug] network_init (%s)\n", servername);
	#endif

	if (!connected) {		
		struct addrinfo hints;
		int error;

		memset(&hints, 0, sizeof hints);
		hints.ai_family = AF_INET;
		hints.ai_socktype = SOCK_DGRAM;

		error = getaddrinfo(servername, CTXSTR(SERVER_PORT), &hints, &serveraddr);
		if (error != 0) bailout ("ss", "could not get addrinfo for ", servername);
		sockhdl = socket(serveraddr->ai_family, serveraddr->ai_socktype, serveraddr->ai_protocol);
		if (sockhdl < 0) bailout ("sd", "could not create client dgram socket, errno=", errno);
		error = connect(sockhdl, serveraddr->ai_addr, serveraddr->ai_addrlen);
		if (error != 0) bailout ("ss", "could not connect() to ", servername);
		freeaddrinfo (serveraddr);

		seq = 1;
		seq_in = 0;
		connected = 1;
		my_uid = rand();
		disc_countdown = DISC_TIMELIMIT;
		pulse_countdown = PULSE_INTERVAL;
	}
}

/* no-op message (I'm still online) */
void send_pulse ()
{
	CtoSmessage pulsemsg;
	memset (&pulsemsg, 0, sizeof(pulsemsg));
	pulsemsg.type = CtoS_PULSE;
	send_CtoSmessage (&pulsemsg);
}

void network_update ()
{
	disc_countdown --;
	pulse_countdown --;
	
	if (disc_countdown <= 0) bailout ("s", "Disconnected from server.");
	
	if (pulse_countdown <= 0) {
		send_pulse ();
		pulse_countdown = PULSE_INTERVAL;
	}
}

void network_cleanup ()
{
	if (shutdown(sockhdl, SHUT_RDWR) != 0)
		bailout ("s", "could not shutdown socket");
	
	if (close(sockhdl) != 0)
		bailout ("s", "could not close socket");
	
	freeaddrinfo (serveraddr);
	connected = 0;
}

/* The following 4 functions are called when the corresponding message is received from the server.
  They go on to call the callback functions defined in network.h.
  Join callbacks are called after the join, leave callbacks BEFORE the leave. So the callbacks have
  access to the data (nicknames etc.) either way.
*/
void insert_client (int index, char* nickname)
{
	strcpy (peer[index].nickname, nickname);
	if (notify_clientjoin != 0) notify_clientjoin (index);
}

void insert_player (int owneridx, int index, char* nickname, int character)
{
	owner[index] = owneridx;
	strcpy (player[index].data.nickname, nickname);
	player[index].data.character = character;
	if (notify_playerjoin != 0) notify_playerjoin (index);
}

void remove_client (int index)
{
	int i;
	
	if (notify_clientleave != 0) notify_clientleave (index);
	
	for (i = 0; i < MAX_CLIENTS; i++) {
		if (owner[i] == index) {
			memset (&player[i], 0, sizeof (Player));
		}
	}
	memset (&peer[index], 0, sizeof (Client));
}

void remove_player (int index)
{
	if (notify_playerleave != 0) notify_playerleave (index);
	memset (&player[index], 0, sizeof (Player));
}

/* ALL SEND AND RECEIVE METHODS transform the structs that are sent back and
  forth into a TEXT BASED human readable protocol and parse it. */

/* Send client to server message. UID in msg is ignored and my_uid is substituted! */
void send_CtoSmessage (CtoSmessage* msg)
{
	int error;
	char buffer [BUFSIZE];
	memset (buffer, 0, sizeof(buffer));
	
	int written;
	
	switch (msg->type) {
	
	case CtoS_PULSE:
		sprintf (buffer, "%d 0 PULSE", my_uid);
		break;

	case CtoS_HELLO:
		sprintf (buffer, "%d 0 HELLO \"%s\"\n", my_uid, msg->nickname);
		break;

	case CtoS_REPEAT:
		sprintf (buffer, "%d 0 REPEAT %d\n", my_uid, msg->seq);
		break;
	
	case CtoS_CHAT:  /* (char* line) contains chat message to be sent to everyone */
		sprintf (buffer, "%d %d CHAT \"%s\"\n", my_uid, seq, msg->line);
		break;
	
	case CtoS_LOBBY_ADDPLAYER:
		sprintf (buffer, "%d %d LOBBY_ADDPLAYER \"%s\" %d\n", my_uid, seq, msg->data.nickname, msg->data.character);
		break;
	
	case CtoS_LOBBY_SETPLAYERDATA:
		sprintf (buffer, "%d %d LOBBY_SETPLAYERDATA %d \"%s\" %d\n", my_uid, seq, msg->slot, msg->data.nickname, msg->data.character);
		break;
	
	case CtoS_LOBBY_REMOVEPLAYER:
		sprintf (buffer, "%d %d LOBBY_REMOVEPLAYER %d\n", my_uid, seq, msg->slot);
		break;
	
	case CtoS_LOBBY_READY:
		sprintf (buffer, "%d %d LOBBY_READY\n", my_uid, seq);
		break;
	
	case CtoS_LOBBY_NOTREADY:
		sprintf (buffer, "%d %d LOBBY_NOTREADY\n", my_uid, seq);
		break;
	
	case CtoS_INGAME_ACTION:
		written = sprintf (buffer, "%d %d INGAME_ACTION %d %d %d %d %d %d %d", 
			my_uid, seq, msg->action.type, msg->action.actor, msg->action.start_time,
			msg->action.location.x, msg->action.location.y, msg->action.location.z,
			msg->action.dir);
		
		switch (msg->action.type) { // append action type specific data
			case ACTION_ATTACK_CLOSE:
				sprintf (buffer+written, " %d\n", msg->action.cattack->weapon);
				break;
			
			case ACTION_ATTACK_RANGE:
				sprintf (buffer+written, " %d\n", msg->action.rattack->weapon);
				break;
		
			default: sprintf (buffer+written, "\n"); break;
		}
		break;
	
	case CtoS_INGAME_SETINVENTORY:
		sprintf (buffer, "%d %d INGAME_SETINVENTORY %d %d %d %d %d %d\n", my_uid, seq, msg->player, msg->inv.slot[0], msg->inv.slot[1], msg->inv.slot[2], msg->inv.slot[3], msg->inv.slot[4]);
		break;
	
	default:
		bailout("sds", "cannot send_CtoSmessage type ", msg->type, " (unknown)");
		break;
	
	}
	
	#if TOWERHACK_DEBUG
	if (msg->type != CtoS_PULSE) fprintf (stderr, "[Debug] SEND CtoS: %s\n", buffer);
	#endif
	
	error = send (sockhdl, buffer, sizeof(buffer), 0);
	if (error != sizeof(buffer)) bailout ("s", "error on send data to server");
	
	// decide whether to increment seq
	switch (msg->type) {
		case CtoS_PULSE:
		case CtoS_HELLO:
			break;
		default:
			seq++;
	}
}

void send_repeat (int seq)
{
	CtoSmessage repeatmsg;
	memset (&repeatmsg, 0, sizeof(repeatmsg));
	repeatmsg.type = CtoS_REPEAT;
	repeatmsg.seq = seq;
	send_CtoSmessage (&repeatmsg);
}
	
StoCmsgType parsetype (char** token)
{
	char* typestr = *token;
	while (isspace(*typestr)) typestr++;
	
	#define TRYPARSE(STR) if (strncmp(typestr, #STR, sizeof(#STR)-1) == 0) { *token = typestr + sizeof(#STR)-1; return StoC_##STR; }
	
	TRYPARSE(PULSE) else
	TRYPARSE(KICK) else
	TRYPARSE(NOTIFY) else
	TRYPARSE(CHAT) else
	TRYPARSE(ALLINFO) else
	TRYPARSE(LOBBY_NEWCLIENT) else
	TRYPARSE(LOBBY_NEWPLAYER) else
	TRYPARSE(LOBBY_PLAYERLEAVE) else
	TRYPARSE(LOBBY_CLIENTLEAVE) else
	TRYPARSE(LOBBY_SEEDGAME) else
	TRYPARSE(LOBBY_CLIENTREADY) else
	TRYPARSE(LOBBY_CLIENTNOTREADY) else
	TRYPARSE(LOBBY_STARTGAME) else
	TRYPARSE(INGAME_ACTION) else
	TRYPARSE(INGAME_DAMAGE) else
	TRYPARSE(INGAME_DIE) else
	TRYPARSE(INGAME_SETPLAYER) else
	TRYPARSE(INGAME_OVER) else
		#if TOWERHACK_DEBUG
		fprintf (stderr, "[Debug] Failed to parsetype \"%s\".\n", typestr);
		#endif
	return StoC_ILLEGALMSG;
}

void parsestring (char* to, char** from)
{
	char* begin = strchr(*from, '"')+1;
	char* end = strchr(begin, '"');
	strncpy(to, begin, end-begin);
	*(to+(end-begin)) = '\0';
	
	#if TOWERHACK_DEBUG
	// fprintf (stderr, "[Debug] parsestring (to \"%s\", from \"%s\") begin=\"%s\" end=\"%s\"\n\n", to, *from, begin, end);
	#endif
	
	*from = end+1;
}

/* Receive server to client message */
void receive_StoCmessage (StoCmessage* msg)
{
	char buffer [BUFSIZE];
	char* readptr = buffer;
	fd_set set;
	struct timeval timeout;
	int sseq;
	int error;
	
tryagain:	
	readptr = buffer;
	memset (buffer, 0, sizeof(buffer));
	memset (&timeout, 0, sizeof(timeout));
	FD_ZERO (&set);
	FD_SET (sockhdl, &set);
	
	error = select(sockhdl+1, &set, 0, 0, &timeout);
	
	if (error < 0) bailout ("s", "error on select socket");
	if (!FD_ISSET(sockhdl, &set)) {
		msg->type = StoC_NOMSG;
		return;
	}

	error = recv(sockhdl, buffer, sizeof(buffer), 0);
	if (error != sizeof(buffer)) bailout ("s", "error on receive from socket");
	
	sseq = strtol (readptr, &readptr, 10);
	if (sseq != 0) {
		if (sseq > seq_in+1) {
			send_repeat (seq_in+1);
			msg->type = StoC_NOMSG;
	
			#if TOWERHACK_DEBUG
			fprintf (stderr, "[Debug] RECEIVE and REPEAT (out of seq): %s", buffer);
			#endif
			return;
		}
		seq_in++;
	}
	
	disc_countdown = DISC_TIMELIMIT; // do not consider message type for this
	
	msg->type = parsetype (&readptr);
	#if TOWERHACK_DEBUG
	if (msg->type != StoC_PULSE) fprintf (stderr, "[Debug] RECEIVE: %s", buffer);
	#endif
	
	int type, actor, start_time, loc_x, loc_y, loc_z;
	Direction dir;
	
	switch (msg->type) {
	case StoC_PULSE: // only resets disc_coundown (see above), 
		goto tryagain; // do not pass through to outer app layers
	
	case StoC_KICK:
		bailout ("s", "you got kicked from the server.");
		break;
	
	case StoC_NOTIFY:
		parsestring (msg->line, &readptr);
		break;
	
	case StoC_CHAT:
		parsestring (msg->line, &readptr);
		break;
	
	case StoC_ALLINFO:
		msg->allinfo.state = strtol (readptr, &readptr, 10);
		for (int i = 0; i < MAX_CLIENTS; i++)  {
			parsestring (msg->allinfo.peer[i].nickname, &readptr);
			msg->allinfo.peer[i].ready = strtol (readptr, &readptr, 10);
			msg->allinfo.peer[i].uid = strtol (readptr, &readptr, 10);
		}
		for (int i = 0; i < MAX_CLIENTS; i++) {
			parsestring (msg->allinfo.player[i].data.nickname, &readptr);
			msg->allinfo.player[i].data.character = strtol (readptr, &readptr, 10);
		}
		for (int i = 0; i < MAX_CLIENTS; i++)  msg->allinfo.owner[i] = strtol (readptr, &readptr, 10);
		if (msg->allinfo.state != STATE_LOBBY) bailout ("s", "Server is not in lobby so you cannot connect right now.");
		memcpy (peer, msg->allinfo.peer, sizeof(peer));
		memcpy (player, msg->allinfo.player, sizeof(player));
		memcpy (owner, msg->allinfo.owner, sizeof(owner));
		goto tryagain;
		
	case StoC_LOBBY_NEWCLIENT:
		msg->clientidx = strtol (readptr, &readptr, 10);
		parsestring (msg->nickname, &readptr);
		insert_client (msg->clientidx, msg->nickname);
		goto tryagain;
	
	case StoC_LOBBY_NEWPLAYER:
		msg->owner = strtol (readptr, &readptr, 10);
		msg->playeridx = strtol (readptr, &readptr, 10);
		parsestring (msg->player.data.nickname, &readptr);
		msg->player.data.character = strtol (readptr, &readptr, 10);
		insert_player (msg->owner, msg->playeridx, msg->player.data.nickname, msg->player.data.character);
		goto tryagain;
	
	case StoC_LOBBY_PLAYERLEAVE:
		msg->playeridx = strtol (readptr, &readptr, 10);
		remove_player (msg->playeridx);
		goto tryagain;
	
	case StoC_LOBBY_CLIENTLEAVE:
		msg->clientidx = strtol (readptr, &readptr, 10);
		remove_client (msg->clientidx);
		goto tryagain;
	
	case StoC_LOBBY_SEEDGAME:
		msg->seed = strtoul (readptr, &readptr, 10);
		break;
	
	case StoC_LOBBY_CLIENTREADY:
		msg->clientidx = strtol (readptr, &readptr, 10);
		break;		
	
	case StoC_LOBBY_CLIENTNOTREADY:
		msg->clientidx = strtol (readptr, &readptr, 10);
		break;
	
	case StoC_LOBBY_STARTGAME:
		break;
	
	case StoC_INGAME_ACTION:
		type = strtol (readptr, &readptr, 10);
		actor = strtol (readptr, &readptr, 10);
		start_time = strtol (readptr, &readptr, 10);
		loc_x = strtol (readptr, &readptr, 10);
		loc_y = strtol (readptr, &readptr, 10);
		loc_z = strtol (readptr, &readptr, 10);
		dir = strtol (readptr, &readptr, 10);
		
		int weapon;
		switch (type) {

		case ACTION_ATTACK_CLOSE:
			weapon = strtol (readptr, &readptr, 10);
			msg->action = action_alloc_closeattack (0, 0, weapon);
			break;
		
		case ACTION_ATTACK_RANGE:
			weapon = strtol (readptr, &readptr, 10);
			msg->action = action_alloc_rangeattack (0, 0, weapon);
			break;
			
		default:
			msg->action = no_action;
			break;
		
		}
		
		msg->action.type = type;
		msg->action.actor = actor;
		msg->action.start_time = start_time;
		msg->action.location.x = loc_x;
		msg->action.location.y = loc_y;
		msg->action.location.z = loc_z;
		msg->action.dir = dir;
		break;
	
	case StoC_INGAME_DAMAGE:
		msg->damage.time     = strtol (readptr, &readptr, 10);
		msg->damage.where.x  = strtol (readptr, &readptr, 10);
		msg->damage.where.y  = strtol (readptr, &readptr, 10);
		msg->damage.where.z  = strtol (readptr, &readptr, 10);
		msg->damage.attacker = strtol (readptr, &readptr, 10);
		msg->damage.victim   = strtol (readptr, &readptr, 10);
		msg->damage.weapon   = strtol (readptr, &readptr, 10);
		msg->damage.damage   = strtol (readptr, &readptr, 10);
		msg->damage.hp       = strtol (readptr, &readptr, 10);
		break;
	
	case StoC_INGAME_DIE:
		msg->damage.time     = strtol (readptr, &readptr, 10);
		msg->damage.where.x  = strtol (readptr, &readptr, 10);
		msg->damage.where.y  = strtol (readptr, &readptr, 10);
		msg->damage.where.z  = strtol (readptr, &readptr, 10);
		msg->damage.attacker = strtol (readptr, &readptr, 10);
		msg->damage.victim   = strtol (readptr, &readptr, 10);
		msg->damage.weapon   = strtol (readptr, &readptr, 10);
		msg->damage.damage   = strtol (readptr, &readptr, 10);
		msg->damage.hp       = strtol (readptr, &readptr, 10);
		break;
		
	case StoC_INGAME_SETPLAYER:
		msg->who = strtol (readptr, &readptr, 10);
		msg->level = strtol (readptr, &readptr, 10);
		msg->hp = strtol (readptr, &readptr, 10);
		msg->inv.slot[0] = strtol (readptr, &readptr, 10);
		msg->inv.slot[1] = strtol (readptr, &readptr, 10);
		msg->inv.slot[2] = strtol (readptr, &readptr, 10);
		msg->inv.slot[3] = strtol (readptr, &readptr, 10);
		msg->inv.slot[4] = strtol (readptr, &readptr, 10);
		break;
	
	case StoC_INGAME_OVER:
		msg->winner = strtol (readptr, &readptr, 10);
		break;
	
	default:
		break;
	
	}
}
